package spark.network;

import org.apache.log4j.Logger;

import java.net.DatagramSocket;
import java.net.SocketException;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.io.IOException;

import spark.network.protocol.Base;
import spark.network.protocol.Response;
import spark.utility.IncrementalLongIdFactory;

public class Sender {
	private static Logger logger = Logger.getLogger(Sender.class);

	private DatagramSocket socket;
	private HashMap<Long, BufferedPacket<Base>> packetsAwaitingConfirmation = new HashMap<Long, BufferedPacket<Base>>();
	private IncrementalLongIdFactory packetIdFactory = new IncrementalLongIdFactory();
	private long timeAdjustment = 0;

	public Sender(int port) throws SocketException {
		socket = new DatagramSocket(port);
	}

	public Sender() throws SocketException {
		socket = new DatagramSocket();
	}

	/**
	 * Sends the packet utilizing the {@link spark.network.Sender#send(spark.network.protocol.Base, java.net.InetSocketAddress)} method.
	 * Additionally initializes the packets session with the session provided in the destination.
	 * @param packet Packet to send.
	 * @param destination Destination to send the packet to and retrieve the session from.
	 */
	public void send(Base packet, NetworkConnection destination) {
		packet.setSession(destination.getSession());
		send(packet, destination.getResponseAddress());
	}

	/**
	 * Sends a packet to an address. If the packet is a reliable one it is buffered until a response is received or
	 * until a certain amount of time has passed. If within that time limit no response was received the packet is
	 * discarded and the client which should have sent a response gets disconnected.
	 *
	 * @param packet      Packet to send.
	 * @param destination Destination to send the packet to.
	 */
	public void send(Base packet, InetSocketAddress destination) {
		packet.setPacketId(packetIdFactory.getNextId());

		if(packet.isReliable()) {
			BufferedPacket<Base> bufferedPacket = new BufferedPacket<Base>(packet);
			packetsAwaitingConfirmation.put(packet.getPacketId(), bufferedPacket);
		}

		packet.setTimeStampSend(System.currentTimeMillis() + timeAdjustment);
		try {
			socket.send(Util.createPacket(packet, destination));
		}
		catch(IOException e) {
			//TODO : handle this somehow?!
			logger.error(e);
		}
	}

	/**
	 * If the provided packet is a response to a packet we have sent earlier remove the packet which was awaiting
	 * a response from the awaiting response packets list. If it wasn't a response this method does nothing.
	 *
	 * @param response Packet which is a potential response to a packet sent earlier awaiting said response.
	 * @return Packet which was awaiting the response if there was one. null otherwise.
	 */
	public Base updateAwaitingConfirmations(Response response) {
		BufferedPacket bufferedPacket = packetsAwaitingConfirmation.get(response.getResponseToId());
		Base packetAwaitingResponse = null;
		if(bufferedPacket != null) {
			if(response.getRequestTypeClass().isInstance(bufferedPacket.getPacket())) {
				packetAwaitingResponse = bufferedPacket.getPacket();
				packetsAwaitingConfirmation.remove(response.getResponseToId());
			}
			else {
				logger.info("Received a response which has a response id matching that of a waiting packet, however " +
				            "the response is a response to packets of type " + response.getRequestTypeClass() + " while " +
				            "the packet awaiting a response is of type " + bufferedPacket.getPacket().getClass() + ". " +
				            "Will disregard the response.");
				packetAwaitingResponse = null;
			}
		}
		return packetAwaitingResponse;
	}

	public void shutdown() {
		if(!socket.isClosed()) {
			socket.close();
		}
	}

	public void setTimeAdjustment(long timeAdjustment) {
		this.timeAdjustment = timeAdjustment;
	}

	public long getTimeAdjustment() {
		return timeAdjustment;
	}
}

